﻿// ------------------------------------------------------------------------------------------------
//  <copyright file="HtmlProcessor.cs" company="Iveely">
//    Copyright (c) Iveely Liu.  All rights reserved.
//  </copyright>
//  
//  <Create Time>
//    03/02/2013 21:59 
//  </Create Time>
//  
//  <contact owner>
//    liufanping@iveely.com 
//  </contact owner>
//  -----------------------------------------------------------------------------------------------

#region

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using HtmlAgilityPack;

#endregion

namespace IveelySE.Spider
{
    public class HtmlProcessor : IProcessor
    {
        #region IProcessor Members

        public bool CanProcess(BasicContent content)
        {
            return content is HtmlContent;
        }

        public IEnumerable<string> Process(BasicContent content)
        {
            var htmlContent = (HtmlContent) content;
            var doc = htmlContent.Html;

            doc.LoadHtml(EscapeIEConditionalComments(htmlContent.Text));

            var foundUrls = new HashSet<string>();

            CollectUrls(foundUrls, doc, "a", "href");
            CollectUrls(foundUrls, doc, "area", "href");
            CollectUrls(foundUrls, doc, "frame", "src");
            CollectUrls(foundUrls, doc, "iframe", "src");
            CollectUrls(foundUrls, doc, "img", "src");
            CollectUrls(foundUrls, doc, "input", "src");
            CollectUrls(foundUrls, doc, "link", "href");
            CollectUrls(foundUrls, doc, "object", "data");
            CollectUrls(foundUrls, doc, "script", "src");

            doc.LoadHtml(UnescapeIEConditionalComments(htmlContent.Text));

            return foundUrls;
        }

        #endregion

        #region IE Conditional Support

        private const string _IEConditionalReplaceString = "$1--$2";
        private const string _EscapedIEConditionalReplaceString = "$1$3";

        private static readonly Regex _IEConditionalStartRegex = new Regex(@"(<!--\[if(?:.*?)IE(?:.*?)\])(>)",
                                                                           RegexOptions.Compiled |
                                                                           RegexOptions.Singleline);

        private static readonly Regex _IEConditionalEndRegex = new Regex(@"(<!)(\[endif\]-->)",
                                                                         RegexOptions.Compiled | RegexOptions.Singleline);

        private static readonly Regex _EscapedIEConditionalStartRegex = new Regex(
            @"(<!--\[if(?:.*?)IE(?:.*?)\])(--)(>)", RegexOptions.Compiled | RegexOptions.Singleline);

        private static readonly Regex _EscapedIEConditionalEndRegex = new Regex(@"(<!)(--)(\[endif\]-->)",
                                                                                RegexOptions.Compiled |
                                                                                RegexOptions.Singleline);

        protected static string EscapeIEConditionalComments(string input)
        {
            var startingTagsEscaped = _IEConditionalStartRegex.Replace(input, _IEConditionalReplaceString);

            return _IEConditionalEndRegex.Replace(startingTagsEscaped, _IEConditionalReplaceString);
        }

        protected static string UnescapeIEConditionalComments(string input)
        {
            var startingTagsUnescaped = _EscapedIEConditionalStartRegex.Replace(input,
                                                                                _EscapedIEConditionalReplaceString);

            return _EscapedIEConditionalEndRegex.Replace(startingTagsUnescaped, _EscapedIEConditionalReplaceString);
        }

        #endregion

        private static void CollectUrls(HashSet<string> links, HtmlDocument htmlDocument, string elementName,
                                        string attributeName)
        {
            var elements = htmlDocument.DocumentNode.SelectNodes("//" + elementName + "[@" + attributeName + "]");

            if(elements == null)
            {
                return;
            }

            var urls = elements.Select(e => e.Attributes[attributeName]).Select(a => HttpUtility.HtmlDecode(a.Value));

            AddRange(ref links, urls);
        }

        private static void AddRange<T>(ref HashSet<T> hashSet, IEnumerable<T> collection)
        {
            if(hashSet == null)
            {
                throw new ArgumentNullException("hashSet");
            }

            if(collection == null)
            {
                throw new ArgumentNullException("collection");
            }

            foreach(var item in collection)
            {
                hashSet.Add(item);
            }
        }
    }
}